// TODO: CODE REVIEW & CLEANUP!
//
// AggregatorDriver.cs
//
// Implements HomeUX.Components.AggregatorDriver.
//
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Xml;
using HomeUX.DriverModel;
using HomeUX.Networking;
using HomeUX.Synchronization;
using HomeUX.Utilities;

namespace HomeUX.Components
{

/// <summary>
/// The built-in "Aggregator" driver that implements "&lt;Aggregator&gt;" pseudo-devices.
/// </summary>
///
public class AggregatorDriver : Driver
{
    Dictionary<SyncPath, AggregatorProperty> _mapNameToProperty = new Dictionary<SyncPath, AggregatorProperty>();
    Dictionary<SyncPath, AggregatorController> _mapPathToController = new Dictionary<SyncPath, AggregatorController>();
    //Dictionary<string, AggregatorControlledDevice> _mapDeviceNameToInfo = new Dictionary<string, AggregatorControlledDevice>();

    // maps aggregator function name to an AggregatorFunctionFactory which can create AggregatorFunction
    // instances of that function
    Dictionary<string, AggregatorFunctionFactory> _aggregatorFunctionFactories =
        new Dictionary<string, AggregatorFunctionFactory>();

    internal AggregatorFunction CreateAggregatorFunction(AggregatorDriver aggregator, string functionName,
        Dictionary<string, string> functionArguments, AggregatorController controller, ConfigurationElement element)
    {
        AggregatorFunctionFactory factory;
        if (!_aggregatorFunctionFactories.TryGetValue(functionName, out factory))
        {
            throw new ConfigurationException(element, "Aggregator function \"{0}\" not found",
                functionName);
        }

        AggregatorFunction function;
        try
        {
            function = factory.CreateAggregatorFunction(aggregator, functionArguments, controller);
        }
        catch (FatalException ex)
        {
            throw new ConfigurationException(ex, element, "Error creating aggregator function \"{0}\"",
                functionName);
        }

        if (functionArguments.Count > 0)
        {
            throw new ConfigurationException(element,
                "Aggregator function \"{0}\" does not have a paramater named \"{1}\"",
                functionName, functionArguments.Keys.First());
        }

        return function;
    }

    public IDictionary<SyncPath, AggregatorProperty> Properties
    {
        get
        {
            return new ReadOnlyDictionary<SyncPath,AggregatorProperty>(_mapNameToProperty);
        }
    }

    protected override void OnInitialize()
    {
        // initialize built-in aggregator functions
        AddAggregatorFunctionFactory("CopyProperty", new CopyPropertyFactory());
        //AddAggregatorFunctionFactory("IfProperty", new IfPropertyFactory());

        // create functions
        foreach (ConfigurationElement element in Configuration.Children)
        {
            try
            {
                if (element.Name == "Function")
                    CreateFunction(element);
            }
            catch (SyncException ex) // e.g. from AggregatorProperty.Value
            {
                throw new ConfigurationException(ex, element, "Failed to initialize AggregatorDriver functions");
            }
        }

        // enumerate child elements
        foreach (ConfigurationElement element in Configuration.Children)
		{
            try
            {
                switch (element.Name)
                {

                case "NumericProperty":

                    CreateNumericProperty(element);
                    break;

                case "BooleanProperty":

                    CreateBooleanProperty(element);
                    break;

                case "EnumerationProperty":

                    CreateEnumerationProperty(element);
                    break;

                case "ScriptProperty":

                    CreateScriptProperty(element);
                    break;

                }
            }
            catch (SyncException ex) // e.g. from AggregatorProperty.Value
            {
                throw new ConfigurationException(ex, element, "Failed to initialize AggregatorDriver properties");
            }
		}

        // create controllers
        foreach (ConfigurationElement element in Configuration.Children)
        {
            try
            {
                if (element.Name == "Control")
                    CreateController(element);
            }
            catch (SyncException ex) // e.g. from AggregatorProperty.Value
            {
                throw new ConfigurationException(ex, element, "Failed to initialize AggregatorDriver controllers");
            }
        }

        // set each AggregatorController.InitialValue to the value that the SyncNode controlled by
        // that controller should be set to
        foreach (AggregatorController controller in _mapPathToController.Values)
		{
			// note that null means "no initial value"
            controller.InitialValue = controller.Function.Evaluate();
		}

        foreach (AggregatorProperty property in _mapNameToProperty.Values)
        {
            foreach (AggregatorController controller in property.AffectedControllers)
                controller.AffectedByProperties[property.Path] = property;

            var scriptProperty = property as ScriptAggregatorProperty;
            if (scriptProperty != null)
                scriptProperty.CalculateValue();
        }
    }

    public void AddAggregatorFunctionFactory(string name, AggregatorFunctionFactory factory)
    {
        _aggregatorFunctionFactories.Add(name, factory);
    }

    //bool AreControlledDevicesEnabled()
    //{
    //    SyncNodeBase syncTree = PseudoClientTree;
    //    var deviceStatusPath = SyncPath.FromString("DeviceStatus");
    //    foreach (string deviceName in _mapDeviceNameToSyncNode.Keys)
    //    {
    //        // oops: wrong path!
    //        SyncNodeBase syncNode = syncTree.CreateNode(SyncPath.Build(deviceStatusPath, deviceName));
    //        if (!Object.Equals(syncNode.Value, true))
    //            return false;
    //    }
    //    return true;
    //}

    protected override void OnEnable()
    {
        if (PersistedTree != null)
        {
            // this is the first OnEnable call

            foreach (AggregatorController controller in _mapPathToController.Values)
				controller.AttemptToInitializeIfNeeded(false);

            //foreach (AggregatorControlledDevice device in _mapDeviceNameToInfo.Values)
            //    InitializeControlledDevice(device);
        }

        // the Aggregator device can be enabled immediately (synchronously) -- its
        // AggregatorProperty objects are individually enabled or disabled as needed based
        // on the SyncNodes they control)
        EnableCompleted(true);

        // the enabled/disabled state of each AggregatorProperty depends, in part, on the
        // enabled/disabled state of the Aggregator itself, so when the latter changes we
        // need to recalculate the former
        RecalculateAllPropertyEnabledStates();
    }

    protected override void OnDisable()
    {
        // cancel all parallel requests
        while (_requests.Count > 0)
            _requests[0].OnExit();

        // the Aggregator device can be disabled immediately (synchronously)
        DisableCompleted();

        // the enabled/disabled state of each AggregatorProperty depends, in part, on the
        // enabled/disabled state of the Aggregator itself, so when the latter changes we
        // need to recalculate the former
        RecalculateAllPropertyEnabledStates();
    }

    void RecalculateAllPropertyEnabledStates()
    {
        foreach (AggregatorProperty property in _mapNameToProperty.Values)
            property.MustRecalculateIsEnabled = true;
        CheckPropertyDirtyStatusAsync();
    }

    public override void SerializeStateForClient(Guid sessionId, XmlWriter writer)
    {
        var update = new SyncNodeUpdate();
        foreach (KeyValuePair<SyncPath, AggregatorProperty> kvp in _mapNameToProperty)
        {
            SyncPath path = kvp.Key;
            AggregatorProperty property = kvp.Value;
            update.Path = path;
            update.Value = (IsEnabled && property.IsEnabled) ? property.Value : null;
            update.Serialize(writer, false);

            var enumProperty = property as EnumerationAggregatorProperty;
            if (enumProperty != null)
            {
                SyncPath valuesPath = SyncPath.Build("Configuration", path, "Values");
                foreach (AggregatorEnumValue enumValue in enumProperty.ValidValues.Values)
                {
                    update.Path = SyncPath.Build(valuesPath, enumValue.Name);
                    update.Value = enumValue.Value;
                    update.Serialize(writer, false);
                }
            }
        }
    }

    List<AggregatorSyncNodeUpdateRequest> _requests = new List<AggregatorSyncNodeUpdateRequest>();

    AggregatorProperty SyncPathToProperty(SyncPath syncPath)
    {
        try
        {
            return _mapNameToProperty[syncPath];
        }
        catch (KeyNotFoundException)
        {
            throw new SyncException(
                "SyncPath \"{0}\" is invalid because this aggregator has no property with Name=\"{1}\"",
                syncPath, String.Join(@"\", syncPath.Parts.Skip(1).ToArray()));
        }
    }

    public override void ProcessSyncNodeUpdate(Guid originator, SyncNodeUpdate update, MessageBuilder replyBuilder)
    {
        AggregatorProperty property = SyncPathToProperty(update.Path);

        var scriptProperty = property as ScriptAggregatorProperty;
        if (scriptProperty != null)
        {
            scriptProperty.ProcessSyncNodeUpdate(originator, update, replyBuilder);
            return;
        }

        property.Value = update.Value;

        AggregatorSyncNodeUpdateRequest request;

        request = _requests.FirstOrDefault(r => r.Property.Path == update.Path);
        if (request == null)
        {
            request = new AggregatorSyncNodeUpdateRequest()
            {
                Driver = this,
                Property = property,
            };
            _requests.Add(request);
        }
        request.Originators.Add(originator);

        foreach (AggregatorController controller in property.AffectedControllers)
        {
            object result = controller.Function.Evaluate();
            Trace("Function at line {0} returned: {1}", controller.ConfigurationElement.LineNumber, result);
            if (result != null)
            {
                if (!object.Equals(controller.SyncNode.Value, result))
                {
                    Trace("\"{0}\" set to: {1}", controller.SyncNode.Path, result);
                    // TODO: if we're not online, this does nothing:
                    controller.SyncNode.Value = result;
                    request.WaitForSyncNode(controller.SyncNode);
                }
                else
                    Trace("\"{0}\" already set to: {1}", controller.SyncNode.Path, result);
            }
        }

        request.CheckIfDone();
    }

    public override void ProcessSyncNodeCommand(Guid originator, SyncNodeCommand command, MessageBuilder replyBuilder)
    {
        AggregatorProperty property = SyncPathToProperty(command.Path);

        var scriptProperty = property as ScriptAggregatorProperty;
        if (scriptProperty != null)
        {
            scriptProperty.ProcessSyncNodeCommand(originator, command, replyBuilder);
            return;
        }
        
        // give standard error about not supporting SyncNode commands
        throw new SyncException("Aggregator property \"{0}\" (line {1} of the configuration file) does not support commands because it is not a <ScriptProperty>",
            property.Name, property.ConfigLineNumber);
    }

    [DebuggerDisplay("AggregatorSyncNodeUpdateRequest {Update.Path} -> {Update.Value}")]
    class AggregatorSyncNodeUpdateRequest
    {
        public AggregatorDriver Driver;
        //public SyncNodeUpdate Update;
        public AggregatorProperty Property;
        public List<Guid> Originators = new List<Guid>();

        List<SyncNode> _pendingNodes = new List<SyncNode>();

        public void WaitForSyncNode(SyncNode syncNode)
        {
            if (syncNode.IsWaiting)
            {
                _pendingNodes.Add(syncNode);
                syncNode.IsWaitingChanged += SyncNode_IsWaitingChanged;
            }
        }

        void SyncNode_IsWaitingChanged(object sender, IsWaitingChangedEventArgs e)
        {
            var syncNode = (SyncNode)sender;
            if (!_pendingNodes.Remove(syncNode))
                throw new InvalidOperationException("Internal error");
            syncNode.IsWaitingChanged -= SyncNode_IsWaitingChanged;
            CheckIfDone();
        }

        internal void CheckIfDone()
        {
            if (_pendingNodes.Count == 0)
            {
                // request is complete
                OnExit();
            }
        }

        internal void OnExit()
        {
            while (_pendingNodes.Count > 0)
            {
                SyncNode syncNode = _pendingNodes[0];
                syncNode.IsWaitingChanged -= SyncNode_IsWaitingChanged;
                _pendingNodes.RemoveAt(0);
            }

            var update = new SyncNodeUpdate()
            {
                Path = Property.Path,
                Value = Property.Value
            };
            Driver.BroadcastSyncNodeOperation(update, Originators);
            if (!Driver._requests.Remove(this))
                throw new InvalidOperationException("Internal error");
        }
    }

	void CreateFunction(ConfigurationElement element)
	{
		// can't check for extras here -- only driver knows what attributes are expected
		string functionName = element.GetRequiredAttribute("Name");
		string constructionSpec = element.GetRequiredAttribute("Factory");
		AggregatorFunctionFactory factory;
		try
		{ 
			factory = Util.CreateInstance<AggregatorFunctionFactory>(constructionSpec);
		}
		catch (CreateInstanceException ex)
		{
			throw new ConfigurationException(ex, element,
				"Failed to load AggregatorFunction factory: {0}",
				constructionSpec);
		}

		factory.SetRootConfigurationElement(element);

		try
		{
			factory.OnInitialize(element);
		}
		catch (Exception ex)
		{
			if (ex is FatalException)
			{
				throw new FatalException(ex,
					"Error initializing factory for aggregator function \"{0}\"",
					functionName);
			}
			else
				throw;
		}

		//factory.CheckForExtraAttributes();

		try
		{
			AddAggregatorFunctionFactory(functionName, factory);
		}
		catch (ArgumentException)
		{
			throw new ConfigurationException(element,
				"Duplicate aggregator function name \"{0}\"",
				functionName);
		}
	}

    void InitializePropertyValueFromPersistedTree(AggregatorProperty property)
	{
        SyncNodeBase syncNode = PersistedTree.Parent.TryFindNode(property.Path);
        if ((syncNode != null) && (syncNode.Value != null))
        {
            try
            {
                property.Value = syncNode.Value;
            }
            catch (SyncException ex)
            {
                LogWarning("Failed to set property \"{0}\" to value \"{1}\": {2}",
                    property.Name, syncNode.Value, Util.FormatExceptionMessage(ex));
            }
        }
	}

	void CreateNumericProperty(ConfigurationElement element)
	{
        var property = new NumericAggregatorProperty()
		{
            Aggregator = this,
            ConfigLineNumber = element.LineNumber,
            Path = SyncPath.Build(Name, element.GetRequiredAttribute("Name")),
            NativeValue = element.GetRequiredDoubleAttribute("Default")
		};

		InitializePropertyValueFromPersistedTree(property);
        AddProperty(property, element);
	}

    void CreateBooleanProperty(ConfigurationElement element)
    {
        var property = new BooleanAggregatorProperty()
        {
            Aggregator = this,
            ConfigLineNumber = element.LineNumber,
            Path = SyncPath.Build(Name, element.GetRequiredAttribute("Name")),
            NativeValue = element.GetRequiredBooleanAttribute("Default")
        };

        InitializePropertyValueFromPersistedTree(property);
        AddProperty(property, element);
    }

	void CreateEnumerationProperty(ConfigurationElement element)
	{
        EnumerationAggregatorProperty property;
        property = new EnumerationAggregatorProperty()
		{
			Aggregator = this,
            ConfigLineNumber = element.LineNumber,
            Path = SyncPath.Build(Name, element.GetRequiredAttribute("Name"))
		};

        // enumerate child "<EnumValue>" elements
        foreach (ConfigurationElement childElement in element.Children)
		{
			if (childElement.Name == "EnumValue")
				CreateEnumValue(childElement, property);
		}
        if (property.ValidValues.Count == 0)
        {
            throw new ConfigurationException(element,
                "An <EnumerationProperty> must contain at least one child <EnumValue>");
        }
        else
            property.Value = property.ValidValues.Values.First().Name;

        property.Value = element.GetRequiredAttribute("Default");
        InitializePropertyValueFromPersistedTree(property);

        AddProperty(property, element);
    }

	void CreateScriptProperty(ConfigurationElement element)
	{
        var property = new ScriptAggregatorProperty()
		{
            Aggregator = this,
            ConfigLineNumber = element.LineNumber,
            Path = SyncPath.Build(Name, element.GetRequiredAttribute("Name"))
		};

        foreach (ConfigurationElement childElement in element.Children)
		{
            if (childElement.Name == "On")
            {
                if (property.OnScript != null)
                    throw new ConfigurationException(childElement, "Duplicate <On> script");
                property.OnScript = ParseScript(property, childElement, true);
            }
            else
            if (childElement.Name == "Off")
            {
                if (property.OffScript != null)
                    throw new ConfigurationException(childElement, "Duplicate <Off> script");
                property.OffScript = ParseScript(property, childElement, false);
            }
            else
            if (childElement.Name == "Command")
            {
                if (property.Commands == null)
                    property.Commands = new Dictionary<string, AggregatorScript>();
                string verb = childElement.GetRequiredAttribute("Verb");
                AggregatorScript script = ParseScript(property, childElement, false);
                try
                {
                    property.Commands.Add(verb, script);
                }
                catch (ArgumentException)
                {
                    throw new ConfigurationException(childElement,
                        "Duplicate <Command Verb=\"{0}\">", verb);
                }
            }
		}

        foreach (AggregatorSyncNodeScriptLine scriptLine in property.DistinctPathSyncNodeScriptLines.Values)
            scriptLine.SyncNode.ValueChanged += scriptLine.SyncNode_ValueChanged;

        AddProperty(property, element);
    }

    AggregatorScript ParseScript(ScriptAggregatorProperty property, ConfigurationElement element, bool isOn)
    {
        var script = new AggregatorScript()
        {
            Property = property,
            ConfigLineNumber = element.LineNumber,
            IsOn = isOn
        };

        string[] lines = (element.Text ?? String.Empty).Split('\n');
        element.WasRead = true;
        for (int lineIndex = 0; lineIndex < lines.Length; lineIndex++)
        {
            string line = lines[lineIndex];

            // remove the comment, if any
            int index = line.IndexOf("//");
            if (index >= 0)
                line = line.Substring(0, index);

            line = line.Trim();

            if (line.Length == 0)
                continue;

            AggregatorScriptLine scriptLine;
            AggregatorSyncNodeScriptLine syncNodeScriptLine;
            bool isPause = false;
            const string Pause = "Pause ";
            if (line == "WaitUntilDone")
            {
                scriptLine = new AggregatorWaitUntilDoneScriptLine();
                syncNodeScriptLine = null;
            }
            else
            if (line.StartsWith(Pause))
            {
                isPause = true;
                string secondsAsString = line.Substring(Pause.Length);
                double seconds;
                try
                {
                    seconds = XmlConvert.ToDouble(secondsAsString);
                }
                catch (Exception ex)
                {
                    if ((ex is FormatException) || (ex is OverflowException))
                    {
                        throw new ConfigurationException(element, lineIndex,
                            "Expecting a number of seconds, got: {0}", secondsAsString);
                    }
                    else
                        throw;
                }

                scriptLine = new AggregatorPauseScriptLine()
                {
                    Script = script,
                    SecondsToPause = seconds
                };
                syncNodeScriptLine = null;
            }
            else
            if ((index = line.IndexOf('=')) > 0)
            {
                string path = line.Substring(0, index).Trim();
                syncNodeScriptLine = new AggregatorSetValueScriptLine()
                {
                    Script = script,
                    SyncNode = (SyncNode) PseudoClientTree.CreateNode(SyncPath.Build(path)),
                    StringValue = line.Substring(index + 1).Trim()
                };
                scriptLine = syncNodeScriptLine;
            }
            else
            if ((index = line.IndexOf(':')) > 0)
            {
                string path = line.Substring(0, index).Trim();
                syncNodeScriptLine = new AggregatorExecuteVerbScriptLine()
                {
                    Script = script,
                    SyncNode = (SyncNode)PseudoClientTree.CreateNode(SyncPath.Build(path)),
                    Verb = line.Substring(index + 1).Trim()
                };
                scriptLine = syncNodeScriptLine;
            }
            else
            {
                throw new ConfigurationException(element, lineIndex,
                    "Invalid script line (expecting \"=\" or \":\"): {0}", line);
            }

            scriptLine.ConfigLineNumber = element.LineNumber + lineIndex;

            if (isPause)
                script.Lines.Add(new AggregatorWaitUntilDoneScriptLine());

            script.Lines.Add(scriptLine);

            if (syncNodeScriptLine != null)
            {
                AggregatorSyncNodeScriptLine otherScriptLine;
                property.DistinctPathSyncNodeScriptLines.TryGetValue(syncNodeScriptLine.SyncNode.Path,
                    out otherScriptLine);
                if (isOn || (otherScriptLine == null) || !otherScriptLine.Script.IsOn)
                {
                    property.DistinctPathSyncNodeScriptLines[syncNodeScriptLine.SyncNode.Path] =
                        syncNodeScriptLine;
                }
            }

            if (isPause)
                script.Lines.Add(new AggregatorWaitUntilDoneScriptLine());
        }

        // add a final "WaitUntilDone" line
        script.Lines.Add(new AggregatorWaitUntilDoneScriptLine());

        return script;
    }

    void CreateEnumValue(ConfigurationElement element, EnumerationAggregatorProperty property)
	{
        AggregatorEnumValue enumValue;
        enumValue = new AggregatorEnumValue()
		{
			Name = element.GetRequiredAttribute("Name")
		};
		// TODO: disallow backslash in <EnumValue Name="...">?

		enumValue.Value = element.GetOptionalAttribute("Value", enumValue.Name);

		string dataString = element.GetOptionalAttribute("Data", null);
		if (dataString != null)
		{
			string[] parts = dataString.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
			foreach (string rawPart in parts)
			{
				string part = rawPart.Trim();
				string[] pair = part.Split('=');
				string name;
				if ((pair.Length != 2) || ((name = pair[0].Trim()).Length == 0))
				{
					throw new ConfigurationException(element,
						"Expected a \"name=value\" pair, got: {0}", part);
				}
				try
				{
					enumValue.Data.Add(name, pair[1]);
				}
				catch (ArgumentException)
				{
					throw new ConfigurationException(element,
						"Duplicate name \"{0}\" in \"name=value\" pairs", name);
				}
			}
		}

		try
		{
			property.ValidValues.Add(enumValue.Name, enumValue);
		}
		catch (ArgumentException)
		{
			throw new ConfigurationException(element,
				"Duplicate <enumValue> name: {0}", enumValue.Name);
		}
	}

    void AddProperty(AggregatorProperty property, ConfigurationElement element)
    {
        try
        {
            _mapNameToProperty.Add(property.Path, property);
        }
        catch (ArgumentException)
        {
            throw new ConfigurationException(element,
                "Duplicate property name: {0}", property.Name);
        }
    }

    void CreateController(ConfigurationElement element)
    {
        SyncNodeBase pseudoClientTree = PseudoClientTree;

        //using (var extras = new ExtraAttributeChecker(element))
        var controller = new AggregatorController()
        {
            Aggregator = this,
            SyncNode = (SyncNode)pseudoClientTree.CreateNode(
                SyncPath.Build(element.GetRequiredAttribute("Path"))),
            ConfigurationElement = element
        };

        controller.SyncNode.ValueChanged += controller.SyncNode_ValueChanged;
        controller.SyncNode.IsWaitingChanged += controller.SyncNode_IsWaitingChanged;

#if false
        string deviceName = controller.SyncNode.Path.GetRequiredPart(0);
        AggregatorControlledDevice device;
        if (!_mapDeviceNameToInfo.TryGetValue(deviceName, out device))
        {
            SyncNode deviceStatusNode = (SyncNode)pseudoClientTree.CreateNode(
                SyncPath.Build("DeviceStatus", deviceName));
            device = new AggregatorControlledDevice
            {
                Aggregator = this,
                Name = deviceName,
                SyncNode = deviceStatusNode,
                IsInitialized = true
            };
            _mapDeviceNameToInfo.Add(deviceName, device);
            deviceStatusNode.ValueChanged += OnControlledDeviceEnabledOrDisabled;
        }

        controller.ControlledDevice = device;

        device.Controllers.Add(controller);
#endif

        //controller.SyncNode.IsWaitingChanged += controller.SyncNode_IsWaitingChanged;
        //controller.SyncNode.ValueChanged += SyncNode_ValueChanged;

        string functionSpec = element.GetRequiredAttribute("Function");

        string functionName;
        Dictionary<string, string> functionArguments;
        ParseFunctionSpec(functionSpec, out functionName,
            out functionArguments, element);

        controller.Function = CreateAggregatorFunction(this, functionName,
            functionArguments, controller, element);

        try
        {
            _mapPathToController.Add(controller.SyncNode.Path, controller);
        }
        catch (ArgumentException)
        {
            throw new ConfigurationException(element,
                "Duplicate <Control> path: {0}", controller.SyncNode.Path);
        }
    }

    internal void CheckPropertyDirtyStatusAsync()
    {
        if (_checkPropertyDirtyStatusWorkItem == null)
            _checkPropertyDirtyStatusWorkItem = WorkQueue.Add(CheckPropertyDirtyStatus);
    }

    WorkItem _checkPropertyDirtyStatusWorkItem;

    void CheckPropertyDirtyStatus()
    {
        _checkPropertyDirtyStatusWorkItem = null;

        MessageBuilder builder = new MessageBuilder();
        foreach (AggregatorProperty property in _mapNameToProperty.Values)
        {
            bool enabledStateHasChanged = false;
            if (property.MustRecalculateIsEnabled)
            {
                property.MustRecalculateIsEnabled = false;
                bool enable = IsEnabled && ShouldPropertyBeEnabled(property);
                if (property.IsEnabled != enable)
                {
                    Trace("Property \"{0}\" is {1}", property.Name, enable ? "enabled" : "disabled");
                    property.IsEnabled = enable;
                    enabledStateHasChanged = true;
                }
            }

            if (!enabledStateHasChanged && !property.MustBroadcastValue)
                continue;

            var update = new SyncNodeUpdate()
            {
                Path = property.Path,
                Value = property.IsEnabled ? property.Value : null
            };
            update.Serialize(builder.Writer, false);
        }
        Message message = builder.GetMessage();
        if (message != null)
            BroadcastMessage(message);
    }

    bool ShouldPropertyBeEnabled(AggregatorProperty property)
    {
        var scriptProperty = property as ScriptAggregatorProperty;
        if (scriptProperty != null)
            return scriptProperty.Value != null;

        foreach (AggregatorController controller in property.AffectedControllers)
        {
            if (controller.State != AggregatorControllerState.Initialized)
            {
                if (controller.SyncNode.Value == null)
                {
                    if (SyncServer.IsDeviceEnabled(controller.SyncNode.TopLevelNode.Name)) // i.e. if controlled device is enabled
                    {
                        Trace(true, "Property \"{0}\" is disabled because SyncNode \"{1}\" is null (see .config line {2}) ",
                            property.Name, controller.SyncNode.Path, controller.ConfigurationElement.LineNumber);
                    }
                }
                return false;
            }
            //if (controller.SyncNode.Value == null)
            //    return false;
        }
        return true;
    }

	// functionSpec is of the form "<function-name>[: <param-name>=<value>]..."
	void ParseFunctionSpec(string functionSpec, out string functionName,
		out Dictionary<string, string> arguments, ConfigurationElement element)
	{
		arguments = new Dictionary<string, string>();
		string[] parts = functionSpec.Split(':');
		if (parts.Length == 1)
			functionName = functionSpec.Trim();
		else
		if (parts.Length == 2)
		{
			functionName = parts[0].Trim();
			string[] pairs = parts[1].Split(';');
			foreach (string rawPairString in pairs)
			{
				string pairString = rawPairString.Trim();
				parts = pairString.Trim().Split('=');
				string argName;
				if ((parts.Length != 2) || ((argName = parts[0].Trim()).Length == 0))
				{
					throw new ConfigurationException(element,
						"Expecting \"parameter=value\", got: {0}", pairString);
				}
				string argValue = parts[1].Trim();
				try
				{
					arguments.Add(argName, argValue);
				}
				catch (ArgumentException)
				{
					throw new ConfigurationException(element,
						"Duplicate parameter name \"{0}\"", argName);
				}
			}
		}
		else
		{
			throw new ConfigurationException(element,
				"Expected \"function-name\" or \"function-name: parameter=value; ...; parameter=value\"; got: {0}",
				functionSpec);
		}
	}

    // expose Trace to helper classes
    internal new void Trace(string format, params object[] args)
    {
        base.Trace(format, args);
    }

    // nested types...

	[DebuggerDisplay("ScriptAggregatorProperty {Name} = {Value}")]
	class ScriptAggregatorProperty : AggregatorProperty
	{
        bool? _value;
		public AggregatorScript OnScript, OffScript;

        internal bool? NativeValue
        {
            get
            {
                return _value;
            }
        }

        /// <summary>
        /// Each element is a script specified by &lt;Command Verb="<i>verb</i>"&gt;, where
        /// <i>verb</i> is the key of the dictionary.  <r>Commands</r> may be <n>null</n> if there
        /// are no commands.
        /// </summary>
        public Dictionary<string, AggregatorScript> Commands;

        /// <summary>
        /// These are all the lines of all scripts in this ScriptAggregatorProperty.  If any two
        /// lines have the same SyncPath, only one is included, using these rules: (a) if one of
        /// the lines is in a "&lt;On&gt;" script and the second isn't, the first is included
        /// (since the lines in "&lt;On&gt;" determine if _value is true or false, whereas the
        /// lines in other scripts are only used to determine if _value is null);
        /// (b) if both are in an "&lt;On&gt;" script, the one that's later in the block is
        /// included (because e.g. "TV\IsPowered = true" followed by a pause, followed by
        /// TV\IsPowered = false" results in _value being true if the TV is off); (c) if
        /// both are in a script other than "&lt;On&gt;", the one that's included is
        /// arbitrary.
        /// </summary>
        public Dictionary<SyncPath, AggregatorSyncNodeScriptLine> DistinctPathSyncNodeScriptLines =
            new Dictionary<SyncPath, AggregatorSyncNodeScriptLine>();

        internal void ProcessSyncNodeUpdate(Guid originator, SyncNodeUpdate update, MessageBuilder replyBuilder)
        {
            bool newValue = update.GetTypedValue<bool>();

            bool updateIsComplete = false;

            if (newValue)
            {
                if (OnScript.IsExecuting)
                    updateIsComplete = true;
                else
                if (OffScript.IsExecuting)
				{
                    OffScript.CancelExecution();
					updateIsComplete = true;
				}
                else
                    OnScript.Execute(originator, update);
            }
            else
            {
                if (OffScript.IsExecuting)
                    updateIsComplete = true;
                else
                if (OnScript.IsExecuting)
				{
                    OnScript.CancelExecution();
					updateIsComplete = true;
				}
                else
                    OffScript.Execute(originator, update);
            }

            if (updateIsComplete)
            {
                // send back "you're done" to <replyBuilder>
                update.OriginatedHere = 1;
                update.Serialize(replyBuilder.Writer, false);
            }
        }

        internal void ProcessSyncNodeCommand(Guid originator, SyncNodeCommand command, MessageBuilder replyBuilder)
        {
            string verb = command.Verb;
            AggregatorScript script;
            if (!Commands.TryGetValue(verb, out script))
            {
                throw new SyncException(
                    "Command \"{0}\" is not defined (<ScriptProperty Name=\"{1}\"> (see line {2} of configuration file) doesn't contain <Command Verb=\"{3}\">)",
                    verb, Name, ConfigLineNumber, verb);
            }

            if (script.IsExecuting)
			{
				script.CancelExecution();
                command.OriginatedHere = 1;
                command.Serialize(replyBuilder.Writer, false);
			}
			else
			{
                script.Execute(originator, command);
			}
        }

        ///// <summary>
        ///// This method is called when AggregatorScriptLine.IsSet changes for any item in Lines.
        ///// </summary>
        //public Action OnScriptLineIsSetChanged;

        /// <summary>
        /// Called when SyncNode.Value changes for any item in DistinctPathSyncNodeScriptLines.
        /// </summary>
        public void OnScriptLineSyncNodeValueChanged(AggregatorSyncNodeScriptLine scriptLineThatChanged)
        {
            CalculateValue();
        }

        public void CalculateValue()
        {
            bool isEnabled = true;
            bool isSet = true;
            int setValueScriptLineCount = 0;
            foreach (AggregatorSyncNodeScriptLine scriptLine in DistinctPathSyncNodeScriptLines.Values)
            {
                AggregatorSetValueScriptLine setValueScriptLine;
                object value = scriptLine.SyncNode.Value;
                if (value == null)
                {
                    isEnabled = false;
                    if (!isSet)
                        break; // optimization
                }
                else
                if (scriptLine.Script.IsOn &&
                    ((setValueScriptLine = scriptLine as AggregatorSetValueScriptLine) != null))
                {
                    setValueScriptLineCount++;
                    if (!object.Equals(setValueScriptLine.NativeValue, value))
                    {
                        isSet = false;
                        if (!IsEnabled)
                            break; // optimization
                    }
                }
            }

            // if a "<ScriptProperty>" has an "<On>" script with no lines of the form "sync-path
            // = value", the overall value of the script property should be false
            if (setValueScriptLineCount == 0)
                isSet = false;

            bool? newValue;
            if (!isEnabled)
                newValue = null;
            else
                newValue = isSet;

            if (_value != newValue)
            {
                Aggregator.Trace("ScriptAggregatorProperty \"{0}\" changed from [{1}] to [{2}]",
                    Name, _value, newValue);
                _value = newValue;
                MustRecalculateIsEnabled = true; // enabled state may have changed -- check it
                MustBroadcastValue = true;
                Aggregator.CheckPropertyDirtyStatusAsync();
            }
        }

		public override object Value
		{
			get
			{
				return _value;
			}
			set
			{
                throw new InvalidOperationException("ScriptAggregatorProperty.Value is read-only");
                //try
                //{
                //    _value = (bool)value;
                //}
                //catch (InvalidCastException)
                //{
                //    throw new SyncException(
                //        "Aggregator property \"{0}\" is Script; its value must be set to a Boolean value; cannot set its value to \"{1}\"",
                //        Name, value);
                //}
			}
		}
	}

	[DebuggerDisplay("{DebuggerDisplay}")]
	class AggregatorScript
	{
        public ScriptAggregatorProperty Property;
        public int ConfigLineNumber; // line number within .config file
        public bool IsOn; // true if this is an "<On>" script
		public List<AggregatorScriptLine> Lines = new List<AggregatorScriptLine>();
        int _nextLineToExecute = -1; // index within Lines
        int _scriptLinesExecuting = 0;

        Guid _originator; // which client initiated script execution
        SyncNodeOperation _syncNodeOperation; // SyncNodeUpdate or SyncNodeCommand that initiated script execution

        public bool IsExecuting
        {
            get
            {
                return _nextLineToExecute >= 0;
            }
        }

        public void Execute(Guid originator, SyncNodeOperation syncNodeOperation)
        {
            if (_nextLineToExecute != -1)
                throw new InvalidOperationException("Script is already executing");
            Property.Aggregator.BusyCount++;
            _nextLineToExecute = 0;
            _originator = originator;
            _syncNodeOperation = syncNodeOperation;
            ContinueExecution();
        }

        public void CancelExecution()
        {
            foreach (AggregatorScriptLine scriptLine in Lines)
            {
                if (scriptLine.IsExecuting)
                    scriptLine.CancelExecution();
            }

            // indicate that the update failed
            var update = _syncNodeOperation as SyncNodeUpdate;
            if (update != null)
                update.Value = Message.FailedValue;

			AfterExecution();
        }

        void AfterExecution()
        {
            Property.Aggregator.BroadcastSyncNodeOperation(_syncNodeOperation, _originator);
            _nextLineToExecute = -1;
            _originator = Guid.Empty;
            _syncNodeOperation = null;
            _scriptLinesExecuting = 0;
            Property.Aggregator.BusyCount--;
        }

        void ContinueExecution()
        {
            while (_nextLineToExecute < Lines.Count)
            {
                AggregatorScriptLine scriptLine = Lines[_nextLineToExecute++];

                if (scriptLine is AggregatorWaitUntilDoneScriptLine)
                {
                    if (_scriptLinesExecuting == 0)
                        continue;
                    else
                        return;
                }

                scriptLine.Execute();
                if (scriptLine.IsExecuting) // i.e. if it didn't complete synchronously
                    _scriptLinesExecuting++;
            }

            // script is done
            if (_scriptLinesExecuting > 0)
                throw new InvalidOperationException("Internal error");
            AfterExecution();
        }

        internal void OnScriptLineExecutionCompleted(AggregatorScriptLine scriptLine)
        {
            if (--_scriptLinesExecuting == 0)
                ContinueExecution();
        }

		object DebuggerDisplay
		{
			get
			{
				return new EncapsulatedString("AggregatorScript (Lines.Count = {0})", Lines.Count);
			}
		}
	}

	abstract class AggregatorScriptLine
	{
		public AggregatorScript Script;
        public int ConfigLineNumber; // line number within .config file
        public bool IsExecuting { get; protected set; }

        public abstract void Execute();
        public abstract void CancelExecution();
    }

    abstract class AggregatorSyncNodeScriptLine : AggregatorScriptLine
    {
        public SyncNode SyncNode;

        internal abstract void SyncNode_ValueChanged(object sender, ValueChangedEventArgs e);

        protected void WaitForSyncNode()
        {
            if (SyncNode.IsWaiting)
            {
                SyncNode.IsWaitingChanged += SyncNode_IsWaitingChanged;
                IsExecuting = true;
            }
        }

        public override void CancelExecution()
        {
            if (!IsExecuting)
                throw new InvalidOperationException("Internal error");
            SyncNode.IsWaitingChanged -= SyncNode_IsWaitingChanged;
            IsExecuting = false;
        }

        void SyncNode_IsWaitingChanged(object sender, IsWaitingChangedEventArgs e)
        {
            if (IsExecuting && !e.IsWaiting)
            {
                IsExecuting = false;
                SyncNode.IsWaitingChanged -= SyncNode_IsWaitingChanged;
                Script.OnScriptLineExecutionCompleted(this);
            }
        }
    }

	[DebuggerDisplay("{DebuggerDisplay}")]
    class AggregatorSetValueScriptLine : AggregatorSyncNodeScriptLine
    {
        // NativeValue has the same type as SyncNode.Value but the
        // value of NativeValue comes from StringValue

        public string StringValue;
		public object NativeValue; // if not null, this is the the parsed form of <StringValue>
		public SyncNodeValueType ValueType; // the type of <NativeValue>
        //public bool IsSet; // true if Value equals SyncNode.Value; not used if Script.UpdateIsSet is null

        public override void Execute()
        {
            if (IsExecuting)
                throw new InvalidOperationException("Internal error");
            CalculateNativeValueIfNecessary();
            Script.Property.Aggregator.Trace("Executing script line {0}: {1} = [{2}]",
                ConfigLineNumber, SyncNode.Path, NativeValue);
            SyncNode.Value = NativeValue;
            WaitForSyncNode();
        }

		internal override void SyncNode_ValueChanged(object sender, ValueChangedEventArgs e)
		{
            // if the type of SyncNode.Value has changed, (re)convert Value to NativeValue
            // because NativeValue is supposed to have the same type as SyncNode.Value but the
            // value of NativeValue comes from StringValue
            CalculateNativeValueIfNecessary();

            Script.Property.OnScriptLineSyncNodeValueChanged(this);
        }

        void CalculateNativeValueIfNecessary()
        {
			if (ValueType != SyncNode.ValueType)
			{
				ValueType = SyncNode.ValueType;
				try
				{
					switch (ValueType)
					{
                    case SyncNodeValueType.Empty:
                        NativeValue = null;
                        break;
					case SyncNodeValueType.Boolean:
						NativeValue = XmlConvert.ToBoolean(StringValue);
						break;
					case SyncNodeValueType.Int32:
						NativeValue = XmlConvert.ToInt32(StringValue);
						break;
					case SyncNodeValueType.Double:
						NativeValue = XmlConvert.ToDouble(StringValue);
						break;
					case SyncNodeValueType.String:
						NativeValue = StringValue;
						break;
					default:
						throw new FormatException();
					}
				}
				catch (Exception ex)
				{
					if ((ex is FormatException) || (ex is OverflowException))
					{
                        Script.Property.Aggregator.ReportAlert(MessageAlertType.Warning,
                            "Error on line {0} of configuration file: cannot convert \"{1}\" to {2}",
                            ConfigLineNumber, StringValue, ValueType);
						NativeValue = null;
						ValueType = SyncNodeValueType.Empty;
					}
					else
						throw;
				}
			}
		}

		object DebuggerDisplay
		{
			get
			{
				return new EncapsulatedString("{0} = {1}", SyncNode.Path, StringValue);
			}
		}
	}

    [DebuggerDisplay("{DebuggerDisplay}")]
    class AggregatorExecuteVerbScriptLine : AggregatorSyncNodeScriptLine
    {
        public string Verb;

        public override void Execute()
        {
            if (IsExecuting)
                throw new InvalidOperationException("Internal error");
            Script.Property.Aggregator.Trace("Executing script line {0}: {1} : {2}",
                ConfigLineNumber, SyncNode.Path, Verb);
            SyncNode.ExecuteCommand(Verb);
            WaitForSyncNode();
        }

        internal override void SyncNode_ValueChanged(object sender, ValueChangedEventArgs e)
        {
            Script.Property.OnScriptLineSyncNodeValueChanged(this);
            //if (Script.Property.OnScriptLineIsSetChanged != null)
            //    Script.Property.OnScriptLineIsSetChanged();
        }

        object DebuggerDisplay
        {
            get
            {
                return new EncapsulatedString("{0} : {1}", SyncNode.Path, Verb);
            }
        }
    }

    // means "wait for previous lines to finish before executing this line (or one derived from this)"
    [DebuggerDisplay("WaitUntilDone")]
    class AggregatorWaitUntilDoneScriptLine : AggregatorScriptLine
    {
        public override void Execute()
        {
            // Execute should not be called for WaitUntilDone
            throw new InvalidOperationException("Internal error");
        }

        public override void CancelExecution()
        {
            // CancelExecution should not be called for WaitUntilDone
            throw new InvalidOperationException("Internal error");
        }
    }

    [DebuggerDisplay("Pause {SecondsToPause} seconds")]
    class AggregatorPauseScriptLine : AggregatorScriptLine
    {
        public double SecondsToPause;
        public WorkItem PauseWorkItem;

        public override void Execute()
        {
            if (IsExecuting)
                throw new InvalidOperationException("Internal error");
            IsExecuting = true;
            Script.Property.Aggregator.Trace("Executing script line {0}: Pause {1}",
                ConfigLineNumber, SecondsToPause);
            PauseWorkItem = Script.Property.Aggregator.WorkQueue.Add((int)(SecondsToPause * 1000), OnPauseComplete);
        }

        void OnPauseComplete()
        {
            if (!IsExecuting)
                throw new InvalidOperationException("Internal error");
            IsExecuting = false;
            PauseWorkItem = null;
            Script.OnScriptLineExecutionCompleted(this);
        }

        public override void CancelExecution()
        {
            if (!IsExecuting)
                throw new InvalidOperationException("Internal error");
            IsExecuting = false;
            PauseWorkItem.Abort();
            PauseWorkItem = null;
        }
    }
}

#if false
[DebuggerDisplay("AggregatorControlledDevice {DeviceName}")]
class AggregatorControlledDevice
{
    public AggregatorDriver Aggregator;
    public string Name;
    public SyncNode SyncNode; // node in pseudo-client "DeviceStatus" tree
    public bool IsInitialized; // true if initialized from AggregatorDriver persisted data
    public bool IsEnabled // true if the controlled device is currently enabled
    {
        get
        {
            return object.Equals(SyncNode.Value, true);
        }
    }
    public List<AggregatorController> Controllers = new List<AggregatorController>();

    // when IsWaiting changes to false on all these nodes, we've completed initialization of
    // this controlled device
	List<SyncNode> _pendingNodes = new List<SyncNode>();

	public void WaitForSyncNode(SyncNode syncNode)
	{
		if (syncNode.IsWaiting)
		{
			_pendingNodes.Add(syncNode);
			syncNode.IsWaitingChanged += SyncNode_IsWaitingChanged;
            if (_pendingNodes.Count == 0)
                Aggregator.OnControlledDeviceInitialized(this);
		}
	}

	void SyncNode_IsWaitingChanged(object sender, IsWaitingChangedEventArgs e)
	{
		var syncNode = (SyncNode)sender;
		if (!_pendingNodes.Remove(syncNode))
			throw new InvalidOperationException("Internal error");
		syncNode.IsWaitingChanged -= SyncNode_IsWaitingChanged;
		if (_pendingNodes.Count == 0)
		{
			// initialization is complete
            IsInitialized = true;
		}
	}
}
#endif

enum AggregatorControllerState
{
	Uninitialized,
	InitializationPending,
	Initialized
}

[DebuggerDisplay("{DebuggerDisplay}")]
public class AggregatorController
{
    public AggregatorDriver Aggregator;
    public SyncNode SyncNode;
	public AggregatorFunction Function;
    public ConfigurationElement ConfigurationElement; // for debugging messages
    //internal AggregatorControlledDevice ControlledDevice;
    internal object InitialValue;
	internal AggregatorControllerState State;
    internal Dictionary<SyncPath, AggregatorProperty> AffectedByProperties =
        new Dictionary<SyncPath, AggregatorProperty>();
    int _suppressSyncNodeEvents;

	internal void AttemptToInitializeIfNeeded(bool tellClients)
	{
		if ((State == AggregatorControllerState.Initialized) ||
		    (State == AggregatorControllerState.InitializationPending))
			return;

		// if we have no value to intialize SyncNode to, /*or if the SyncNode already has the correct
		// initial value$$,*/ initialization is done
        // $$ = don't trust that the device's current value is really what's reflected in the hardware
		if ((InitialValue == null) /*|| Object.Equals(SyncNode.Value, InitialValue)*/)
		{
            Aggregator.BusyCount++;
            OnInitialized(tellClients); // will decrement BusyCount
			return;
		}

		// if the SyncNode is not in a state where it can be initialized, quit
		if (SyncNode.Value == null)
			return;

        // we're now waiting for the SyncNode set operation to complete
        State = AggregatorControllerState.InitializationPending;
        Aggregator.BusyCount++;

        // SyncNode.SetValue doesn't allow changing a value from within a ValueChanged event
        // handler, so the actual initialization needs to be done asynchronously
        Aggregator.WorkQueue.Add(delegate
        {
            Initialize(tellClients);
        });
    }

    void Initialize(bool tellClients)
    {
		// start initializing the SyncNode
        _suppressSyncNodeEvents++;
        try
        {
            SyncNode.Value = InitialValue;
        }
        finally
        {
            _suppressSyncNodeEvents--;
        }

		// if the SyncNode is not in a waiting state, then intialization completed synchronously
		if (!SyncNode.IsWaiting)
		{
			OnInitialized(tellClients);
			return;
		}
    }

	// called when we have completed initialization of <SyncNode>; if <tellClients> is true then
	// tell connected clients if properties that control this controller are now enabled
	void OnInitialized(bool tellClients)
	{
		State = AggregatorControllerState.Initialized;
        Aggregator.BusyCount--;

        foreach (AggregatorProperty property in AffectedByProperties.Values)
            property.MustRecalculateIsEnabled = true;

        if (tellClients)
            Aggregator.CheckPropertyDirtyStatusAsync();
	}

    internal void SyncNode_ValueChanged(object sender, ValueChangedEventArgs e)
    {
        if (_suppressSyncNodeEvents > 0)
            return;

        if (State == AggregatorControllerState.Initialized)
        {
            if (SyncNode.Value == null)
            {
                // the SyncNode became disabled -- reset this controller to uninitialized state
                // so that if/when the SyncNode becomes enabled again we'll re-initialize the
                // SyncNode value
                State = AggregatorControllerState.Uninitialized;
            }
            else
            {
                // SyncNode was already initialized, and now it's getting a new value; save that
                // value away (in <InitialValue>) in case the SyncNode becomes disabled at some
                // point, because if/when it becomes initialized again we'll need to re-initialize
                // it to this value
                InitialValue = SyncNode.Value;
                return;
            }
        }
        else
        {
            // see if the SyncNode needs to be initialized
		    AttemptToInitializeIfNeeded(true);
        }

        // at this point it's possible that this controller's SyncNode became enabled or
        // disabled, which will change the enabled state of all properties that control
        // this SyncNode
        foreach (AggregatorProperty property in AffectedByProperties.Values)
            property.MustRecalculateIsEnabled = true;
        Aggregator.CheckPropertyDirtyStatusAsync();
    }

    internal void SyncNode_IsWaitingChanged(object sender, IsWaitingChangedEventArgs e)
    {
        if (_suppressSyncNodeEvents > 0)
            return;

        if ((State == AggregatorControllerState.InitializationPending) && !SyncNode.IsWaiting)
			OnInitialized(true);
    }

    object DebuggerDisplay
	{
		get
		{
			return new EncapsulatedString("AggregatorController for \"{0}\"",
				SyncNode.Path);
		}
	}
}

public abstract class AggregatorProperty
{
	public AggregatorDriver Aggregator;
	public int ConfigLineNumber; // line number within .config file
    public SyncPath Path { get; internal set; }

    public string Name // unlike Path, this is a string, and doesn't include the aggregator device name
    {
        get
        {
			return String.Join(@"\", Path.Parts.Skip(1).ToArray());
        }
    }

    public List<AggregatorController> AffectedControllers = new List<AggregatorController>(); // TODO: make into a property
    public abstract object Value { get; set; }
    public bool MustBroadcastValue { get; internal set; } // Value needs to be broadcast to clients
    public bool IsEnabled { get; internal set; }
    internal bool MustRecalculateIsEnabled; // if true, IsEnabled needs to be recalculated
}

[DebuggerDisplay("NumericAggregatorProperty {Name} = {Value}")]
public class NumericAggregatorProperty : AggregatorProperty
{
    double _value;

    public double NativeValue
    {
        get
        {
            return _value;
        }
        set
        {
            _value = value;
        }
    }

    public override object Value
    {
        get
        {
            return _value;
        }
        set
        {
            try
            {
                _value = (double)value;
            }
            catch (InvalidCastException)
            {
                throw new SyncException(
                    "Aggregator property \"{0}\" is numeric; cannot set its value to \"{1}\" ({2})",
                    Name, value, value.GetType().Name);
            }
        }
    }
}

[DebuggerDisplay("BooleanAggregatorProperty {Name} = {Value}")]
public class BooleanAggregatorProperty : AggregatorProperty
{
    bool _value;

    public bool NativeValue
    {
        get
        {
            return _value;
        }
        set
        {
            _value = value;
        }
    }

    public override object Value
    {
        get
        {
            return _value;
        }
        set
        {
            try
            {
                _value = (bool)value;
            }
            catch (InvalidCastException)
            {
                throw new SyncException(
                    "Aggregator property \"{0}\" is Boolean; cannot set its value to \"{1}\" ({2})",
                    Name, value, value.GetType().Name);
            }
        }
    }
}

[DebuggerDisplay("EnumerationAggregatorProperty {Name} = {Value}")]
public class EnumerationAggregatorProperty : AggregatorProperty
{
    AggregatorEnumValue _value;

    public Dictionary<string, AggregatorEnumValue> ValidValues = new Dictionary<string, AggregatorEnumValue>();

    public AggregatorEnumValue NativeValue
    {
        get
        {
            return _value;
        }
        set
        {
            _value = value;
        }
    }

    public override object Value
    {
        get
        {
            return _value.Name;
        }
        set
        {
            string enumValueName;
            try
            {
                enumValueName = (string)value;
            }
            catch (InvalidCastException)
            {
                throw new SyncException(
                    "Aggregator property \"{0}\" is enumerated; cannot set its value to \"{1}\" (type {2})",
                    Name, value, value.GetType());
            }

            AggregatorEnumValue enumValue;
            if (!TryGetValue(enumValueName, out enumValue))
            {
                throw new SyncException(
                    "Aggregator property \"{0}\" is enumerated; cannot set its value to \"{1}\"; its value must be one of: {2}",
                    Name, value, String.Join(", ", ValidValues.Keys.ToArray()));
            }

            _value = enumValue;
        }
    }

    bool TryGetValue(string valueName, out AggregatorEnumValue value)
    {
        foreach (AggregatorEnumValue enumValue in ValidValues.Values)
        {
            if (enumValue.Name == valueName)
            {
                value = enumValue;
                return true;
            }
        }

        value = null;
        return false;
    }
}

[DebuggerDisplay("AggregatorEnumValue {Name}")]
public class AggregatorEnumValue
{
    public string Name;
    public string Value;
    public Dictionary<string, string> Data = new Dictionary<string, string>();
}

// the factory for the built-in "CopyProperty" aggregator function
public class CopyPropertyFactory : AggregatorFunctionFactory
{
    public override AggregatorFunction CreateAggregatorFunction(AggregatorDriver aggregator,
        Dictionary<string, string> functionArguments, AggregatorController controller)
    {
        var function = new CopyPropertyFunction()
        {
            Property = GetRequiredAggregatorPropertyArgument<AggregatorProperty>(aggregator,
                functionArguments, "Name", controller),
            DataName = GetOptionalArgument(functionArguments, "DataName")
        };

        if (function.DataName != null)
        {
            var enumProperty = function.Property as EnumerationAggregatorProperty;
            if (enumProperty == null)
            {
                throw new FatalException(
                    "DataName may only specified if the property being copied is an <EnumerationProperty>");
            }
            foreach (AggregatorEnumValue enumValue in enumProperty.ValidValues.Values)
            {
                if (!enumValue.Data.ContainsKey(function.DataName))
                {
                    throw new FatalException(
                        "\"DataName={0}\" is invalid because not all <EnumValue> elements of <EnumerationProperty Name=\"{1}\"> specify a value for \"{0}\" in Data=\"...\"",
                        function.DataName, enumProperty.Path);
                }
            }
        }

        return function;
    }
}

// the built-in "CopyProperty" aggregator function
public class CopyPropertyFunction : AggregatorFunction
{
    public AggregatorProperty Property;
    public string DataName;

    public override object Evaluate()
    {
        if (DataName != null)
        {
            var enumProperty = (EnumerationAggregatorProperty)Property;
            var value = enumProperty.NativeValue.Data[DataName];
            if (value == "-")
                return null; // means "don't change the property"
            else
                return value;
        }
        else
            return Property.Value;
    }
}

#if false
// the factory for the built-in "IfProperty" aggregator function
public class IfPropertyFactory : AggregatorFunctionFactory
{
    public override AggregatorFunction CreateAggregatorFunction(AggregatorDriver aggregator,
        Dictionary<string, string> functionArguments, AggregatorController controller)
    {
        var function = new IfPropertyFunction()
        {
            Property = GetRequiredAggregatorPropertyArgument<AggregatorProperty>(aggregator,
                functionArguments, "Name", controller) as BooleanAggregatorProperty,
            Then = GetRequiredArgument(functionArguments, "Then")
        };

		if (function.Property == null)
			throw new FatalException("Specifid property must be a <BooleanProperty>");

        return function;
    }
}

// the built-in "IfProperty" aggregator function
public class IfPropertyFunction : AggregatorFunction
{
    public BooleanAggregatorProperty Property;
    public string Then;

    public override object Evaluate()
    {
		if (Property.NativeValue)
			return Then;
		else
			return null; // means "don't change the property"
    }
}
#endif

public abstract class AggregatorFunctionFactory
{
    //List<ExtraAttributeChecker> _extraAttributeCheckers = new List<ExtraAttributeChecker>();
    ConfigurationElement _rootElement;

    internal void SetRootConfigurationElement(ConfigurationElement element)
    {
        _rootElement = element;
    }

    public virtual void OnInitialize(ConfigurationElement configurationElement)
    {
        // make sure there are no extra attributes
        //GetExtraAttributeChecker(configurationElement);
    }

#if false
    protected ExtraAttributeChecker GetExtraAttributeChecker(ConfigurationElement element)
    {
        var extras = new ExtraAttributeChecker(element);
        _extraAttributeCheckers.Add(extras);
        if (object.ReferenceEquals(element, _rootElement))
        {
            extras.MarkAttributeAsValid("Name");
            extras.MarkAttributeAsValid("Factory");
        }
        return extras;
    }

    internal void CheckForExtraAttributes()
    {
        foreach (ExtraAttributeChecker extras in _extraAttributeCheckers)
            extras.CheckForExtraAttributes();
    }
#endif

    public abstract AggregatorFunction CreateAggregatorFunction(AggregatorDriver aggregator,
        Dictionary<string, string> functionArguments, AggregatorController controller);

    protected static T ParseEnum<T>(string stringValue, params string[] names)
    {
        for (int index = 0; index < names.Length; index++)
        {
            if (stringValue == names[index])
                return (T)Enum.ToObject(typeof(T), index);
        }
        throw new FatalException("\"{0}\" is not valid; valid values are: {1}",
            stringValue, String.Join(", ", names));
    }

    protected static string GetOptionalArgument(Dictionary<string, string> functionArguments,
        string argumentName)
    {
        string value;
        if (functionArguments.TryGetValue(argumentName, out value))
        {
            functionArguments.Remove(argumentName);
            return value;
        }
        else
            return null;
    }

    protected static string GetRequiredArgument(Dictionary<string, string> functionArguments,
        string argumentName)
    {
        string value = GetOptionalArgument(functionArguments, argumentName);
        if (value == null)
            throw new FatalException("Missing \"{0}\" argument", argumentName);
        return value;
    }

    protected static T GetOptionalAggregatorPropertyArgument<T>(
        AggregatorDriver aggregator, Dictionary<string, string> functionArguments,
        string argumentName, AggregatorController controller)
        where T : AggregatorProperty
    {
        string propertyName = GetOptionalArgument(functionArguments, argumentName);
        if (propertyName == null)
            return null;
        SyncPath syncPath = SyncPath.Build(aggregator.Name, propertyName);

        AggregatorProperty property;
        try
        {
            property = aggregator.Properties[syncPath];
        }
        catch (KeyNotFoundException)
        {
            throw new FatalException("Aggregator property \"{0}\" not found", propertyName);
        }

        property.AffectedControllers.Add(controller);

        try
        {
            return (T)property;
        }
        catch (InvalidCastException)
        {
            string expected;
            if (typeof(T) == typeof(NumericAggregatorProperty))
                expected = "<NumericProperty>";
            else
            if (typeof(T) == typeof(BooleanAggregatorProperty))
                expected = "<BooleanProperty>";
            else
            if (typeof(T) == typeof(EnumerationAggregatorProperty))
                expected = "<EnumerationProperty>";
            else
                expected = typeof(T).Name;
            throw new FatalException("Aggregator property \"{0}\" was found, but it's not of type {1}",
                propertyName, expected);
        }
    }

    protected static T GetRequiredAggregatorPropertyArgument<T>(
        AggregatorDriver aggregator, Dictionary<string, string> functionArguments,
        string argumentName, AggregatorController controller)
        where T : AggregatorProperty
    {
        T property = GetOptionalAggregatorPropertyArgument<T>(aggregator,
            functionArguments, argumentName, controller);
        if (property == null)
            throw new FatalException("Missing \"{0}\" argument", argumentName);
        return property;
    }
}

public abstract class AggregatorFunction
{
    public abstract object Evaluate();
}

}
